﻿/*
NTerm: A .NET based terminal abstraction library
Copyright (C) 2011  Bryan Perris

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

using System;
using System.Text;
using System.IO;

namespace NTerm
{
    /// <summary>
    /// This class provides a layer of abstraction and emulation over the host terminal window.
    /// </summary>
    /// <remarks>
    /// NTerminal assumes nothing changes the host state externally, and that is has full control over the host terminal.
    /// If the host state changed externally, NTerminal must be manually synced with the changed state.  Use NTerminal.ManualBufferWrite to
    /// modifty an area of cell buffer inside NTerminal.  NTerminal only checks if the host buffer resizes on every char write.
    /// 
    /// NTerminal tries to avoid possible errors when controlling the host terminal as much as it can.  NTerminal's safe API could result in 
    /// any thrown exceptions if the RedirectionHandler causes the problem or any underlaying code in the CLR runtime.  Please make sure that the
    /// RedirectionHandler is safe as possible for NTerminal.
    /// </remarks>
    /// <example>
    /// <code>
    /// using System;
    /// using NTerm;
    /// 
    /// class Program
    /// {
    ///     static void Main(string[] args)
    ///     {
    ///         // Initialize NTerminal ready for usage using the built-in I/O host handler
    ///         NTerminal.Initialize();
    ///         
    ///         // Print a message to the screen
    ///         NTerminal.WriteLine("Hello World! Press ENTER to exit.");
    ///         
    ///         // Wait for the user to press ENTER, then exit the program
    ///         NTerminal.ReadLine();
    ///         
    ///         // Release resources used by NTerminal
    ///         NTerminal.Shutdown();
    ///     }
    /// }
    /// </code>
    /// </example>
    public static class NTerminal
    {
        private static HostHandler s_RedirectionHandler;
        private static CellBuffer s_CellBuffer;
        private static ConsoleColor s_CurrentBackgroundColor;
        private static ConsoleColor s_CurrentForgroundColor;
        private static int s_CursorRow;
        private static int s_CursorCol;
        private static int s_MaxRow;
        private static bool s_RefreshOnScroll = false;
		private static bool s_EmulateBufferSize = false;
        private static TextWriter s_OutWriter;
        private static TextWriter s_ErrorWriter;
        private static TextReader s_InReader;


        /// <summary>
        /// Initializes NTerminal with the built-in host handler
        /// </summary>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the host buffer size is smaller than 0 for width, height, or both</exception>
        public static void Initialize()
        {
            s_RedirectionHandler = new DefaultHostHandler();

            Init(s_RedirectionHandler.GetBufferWidth(), s_RedirectionHandler.GetBufferHeight());
        }

        /// <summary>
        /// Initializes NTerminal with a specified host handler
        /// </summary>
        /// <param name="handler">A RedirectionHandler to be used for host control</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the host buffer size is smaller than 0 for width, height, or both</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when the argument handler is null</exception>
        public static void Initialize(HostHandler handler)
        {
            if (handler == null)
                throw new ArgumentNullException("handler");

            Init(s_RedirectionHandler.GetBufferWidth(), s_RedirectionHandler.GetBufferHeight());
        }
		
		/// <summary>
        /// Initializes NTerminal, and sets the default I/O host handler, and emulates the buffersize with the specifed width and height
		/// </summary>
		/// <param name='bufferWidth'>The width of the buffer</param>
		/// <param name='bufferHeight'>The height of the buffer</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the argument bufferWidth and/or bufferHeight is less or equal to 0</exception>
		public static void Initialize(int bufferWidth, int bufferHeight)
		{
            s_RedirectionHandler = new DefaultHostHandler();
			
			s_EmulateBufferSize = true;

            Init(bufferWidth, bufferHeight);
		}

        /// <summary>
        /// Initializes NTerminal, and set the I/O host handler with a specified RedirectionHandler, and emulates the buffersize with the specifed width and height
        /// </summary>
        /// <param name="bufferWidth"></param>
        /// <param name="bufferHeight"></param>
        /// <param name="handler"></param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the host buffer size is smaller than 0 for width, height, or both</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when the argument handler is null</exception>
        public static void Initialize(int bufferWidth, int bufferHeight, HostHandler handler)
        {
            if (handler == null)
                throw new ArgumentNullException("handler");

            s_EmulateBufferSize = true;

            Init(bufferWidth, bufferHeight);
        }

        /// <summary>
        /// Shutdowns NTerminal and releases resources
        /// </summary>
        public static void Shutdown()
        {
            s_CellBuffer = null;
            if (s_RedirectionHandler != null)
            {
                s_RedirectionHandler.Dispose();
                s_RedirectionHandler = null;
            }
        }

        #region /// NTerm Console Methods

        /// <summary>
        /// Tells the host handler to clear the host screen and redraw all characters from the NTerminal buffer.
        /// </summary>
        /// <remarks>
        /// When the buffer contains a lot of lines, the redraw can take a while to finish.  Use only when necessary. 
        /// </remarks>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void ForceConsoleRefresh()
        {
            CheckInitialization();

            s_RedirectionHandler.RefreshOutput(
                        s_CellBuffer.GetCellRegion(new Rectangle(0, 0, s_CellBuffer.BufferWidth, s_MaxRow + 1)));

            s_RedirectionHandler.UpdateCursor(CursorTop, CursorLeft);
        }

        /// <summary>
        /// Causes the buffer to scroll by the number of specified lines
        /// </summary>
        /// <param name="numLines">Number of rows to scroll the buffer</param>
        /// <param name="forceRefresh">Whether to refresh the host screen after scrolling</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void ScrollBuffer(int numLines, bool forceRefresh)
        {
            CheckInitialization();

            Rectangle srcRegion = new Rectangle(0, 1, s_CellBuffer.BufferWidth, s_MaxRow);
            Rectangle clearRegion = new Rectangle(0, s_MaxRow, srcRegion.w, s_CellBuffer.BufferHeight - srcRegion.h);

            for (int i = 0; i < numLines; i++)
            {
                s_CellBuffer.ScrollBuffer(srcRegion, clearRegion, 0, 0, CreateDefaultCell('\0'));
            }

            CursorLeft = 0;
            CursorTop = s_MaxRow;

            if (forceRefresh)
                ForceConsoleRefresh();
        }

        /// <summary>
        /// Updates a region of NTerminal buffer.
        /// </summary>
        /// <remarks>
        /// It is best to avoid needing this since NTerminal should be in full control of the host terminal.
        /// </remarks>
        /// <param name="cells">The cell data to copy to NTerminal</param>
        /// <param name="region">A rectangluar region of where to copy the cells</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void ManualBufferWrite(CharacterCell[,] cells, Rectangle region)
        {
            CheckInitialization();
            s_CellBuffer.FillCellRegion(cells, region);
        }

        /// <summary>
        /// Reads a cell region.
        /// </summary>
        /// <returns>
        /// The cell region.
        /// </returns>
        /// <param name='region'>
        /// Region.
        /// </param>
        /// <exception cref='ArgumentNullException'>
        /// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
        /// </exception>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static CharacterCell[,] ReadCellRegion(Rectangle region)
        {
            CheckInitialization();

            if (region.IsNull())
                throw new ArgumentNullException("region");

            return s_CellBuffer.GetCellRegion(region);
        }

        /// <summary>
        /// Clears the current line
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void ClearLine()
        {
            CheckInitialization();

            CursorLeft = 0;

            for (int i = 0; i < BufferWidth - 1; i++)
            {
                Write(' ');
            }

            CursorLeft = 0;
        }

        #endregion

        #region /// Standard Console Methods ...

        /// <summary>
        /// Reads input when Enter key is hit
        /// </summary>
        /// <returns>The value the user entered into the terminal</returns>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static string ReadLine()
        {
            CheckInitialization();

            return s_RedirectionHandler.ReadLine();
        }

        /// <summary>
        /// Clears the terminal buffer
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void Clear()
        {
            CheckInitialization();

            s_CellBuffer.FillCellRegion(CreateDefaultCell('\0'), new Rectangle(0, 0, s_CellBuffer.BufferWidth, s_CellBuffer.BufferHeight));

            try
            {
                s_RedirectionHandler.ClearScreen();
            }
            catch (System.InvalidOperationException)
            {
                ForceConsoleRefresh();
            }
            catch (System.Exception)
            {
                ForceConsoleRefresh();
            }

            s_MaxRow = 0;
            CursorLeft = 0;
            CursorTop = 0;
        }

        /// <summary>
        /// Sets the size of the buffer.
        /// </summary>
        /// <param name='width'>
        /// Width.
        /// </param>
        /// <param name='height'>
        /// Height.
        /// </param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void SetBufferSize(int width, int height)
        {
            CheckInitialization();

            if (!s_EmulateBufferSize)
            {
                s_RedirectionHandler.SetBufferSize(width, height);
            }

            s_CellBuffer = new CellBuffer(width, height, s_CellBuffer);
        }

        /// <summary>
        /// Resets the color to default
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void ResetColor()
        {
            CheckInitialization();

            s_CurrentForgroundColor = ConsoleColor.Gray;
            s_CurrentBackgroundColor = ConsoleColor.Black;
            s_RedirectionHandler.ResetColor(s_CurrentBackgroundColor, s_CurrentForgroundColor);
        }

        /// <summary>
        /// Sets the current textReader used to read from the Input IO Stream
        /// </summary>
        /// <param name="textReader">An instance of a textreader</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void SetIn(TextReader textReader)
        {
            CheckInitialization();

            s_InReader = textReader;
            s_RedirectionHandler.In = textReader;
        }

        /// <summary>
        /// Sets the current TextWriter used for writing to the error IO stream
        /// </summary>
        /// <param name="textWriter">An instance of a textwriter</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void SetError(TextWriter textWriter)
        {
            CheckInitialization();

            s_ErrorWriter = textWriter;
        }

        /// <summary>
        /// Sets the current TextWriter used for writing to the out IO stream
        /// </summary>
        /// <param name="textWriter">An instance of a textwriter</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void SetOut(TextWriter textWriter)
        {
            CheckInitialization();

            s_OutWriter = textWriter;
        }

        #endregion

        #region /// Write Methods ...

        /// <summary>
        /// Writes value to the terminal
        /// </summary>
        /// <param name="value">the value to write to the terminal</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void Write(Char value)
        {
            BufferWrite(value, true);
        }

        /// <summary>
        /// Writes value to the terminal and appends a newline
        /// </summary>
        /// <param name="value">the value to write to the terminal</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void WriteLine(string value)
        {
            s_OutWriter.WriteLine(value);
        }

        /// <summary>
        /// Writes to terminal with an argument, where {0}, {1}... are replaced by the args, the appends a newline
        /// </summary>
        /// <param name="value">value to write</param>
        /// <param name="args">value argument list</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void WriteLine(string value, params object[] args)
        {
            s_OutWriter.WriteLine(value, args);
        }

        /// <summary>
        /// Writes value to the terminal
        /// </summary>
        /// <param name="value">the value to write to the terminal</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void Write(string value)
        {
            s_OutWriter.Write(value);
        }

        /// <summary>
        /// Writes a value to the terminal, {0}, {1}, ... are replaced by args
        /// </summary>
        /// <param name="format">The value to write to the terminal</param>
        /// <param name="args">value argument list</param>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static void Write(string format, params object[] args)
        {
            s_OutWriter.Write(format, args);
        }

        #endregion

        #region /// Properties ...

		
        /// <summary>
        /// Gets the TextWriter used for the out IO stream
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static TextWriter Out
        {
            get { CheckInitialization(); return s_OutWriter; }
        }

        /// <summary>
        /// Gets the TextWriter used for the error IO stream
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static TextWriter Error
        {
            get { return s_ErrorWriter; }
        }

        /// <summary>
        /// Gets the TextReader used for the in IO stream
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static TextReader In
        {
            get
            {
                CheckInitialization();
                return s_RedirectionHandler.In;
            }
        }

        /// <summary>
        /// Gets and Sets the background color of the terminal
        /// </summary>
        public static ConsoleColor BackgroundColor
        {
            get { return s_CurrentBackgroundColor; }
            set { s_CurrentBackgroundColor = value; }
        }

        /// <summary>
        /// Gets and Sets the foreground color of the terminal
        /// </summary>
        public static ConsoleColor ForegroundColor
        {
            get { return s_CurrentForgroundColor; }
            set { s_CurrentForgroundColor = value; }
        }

        /// <summary>
        /// Sets whether NTerminal should force refresh the console display after the buffer has been scrolled.
        /// This feature is very costly and should avoid, so only enable if the host console window does not 
        /// automically scroll its screen buffer
        /// </summary>
        public static bool RefreshOnScroll
        {
            get { return s_RefreshOnScroll; }
            set { s_RefreshOnScroll = value; }
        }

        /// <summary>
        /// Gets or Sets the column position of the terminal cursor
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static int CursorLeft
        {
            get { return s_CursorCol; }
            set { s_CursorCol = value; CheckInitialization(); UpdateCursorPosition(); }
        }

        /// <summary>
        /// Gets or Sets the row position of the terminal cursor
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static int CursorTop
        {
            get { return s_CursorRow; }
            set
            {
                s_CursorRow = value;
                CheckInitialization();
                UpdateMaxRow();
                UpdateCursorPosition();
            }
        }

        /// <summary>
        /// Gets the width of the terminal's buffer
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static int BufferWidth
        {
            get { CheckInitialization(); return s_CellBuffer.BufferWidth; }
        }

        /// <summary>
        /// Gets the height of the terminal's buffer
        /// </summary>
        /// <exception cref="NTerm.InitializationException">Thrown when NTerminal has not been initialized</exception>
        public static int BufferHeight
        {
            get { CheckInitialization(); return s_CellBuffer.BufferHeight; }
        }

        #endregion

        #region /// Private ...

        private static void UpdateCursorPosition()
        {
            CheckInitialization();
            s_RedirectionHandler.UpdateCursor(CursorTop, CursorLeft);
        }

        private static void CheckInitialization()
        {
            if (s_CellBuffer == null)
                throw new InitializationException("NTerminal has not been initialized.  Please call NTerminal.Initialize()");

            if (s_RedirectionHandler == null)
                throw new InitializationException("NTerminal needs a host handler!");
        }

        private static void CheckBufferSize()
        {
			if (s_EmulateBufferSize)
			{
				return; // Skip automatic size updates, since we have the control instead
			}
			
            if (s_RedirectionHandler.GetBufferWidth() != s_CellBuffer.BufferWidth ||
                s_RedirectionHandler.GetBufferHeight() != s_CellBuffer.BufferHeight)
            {
                s_CellBuffer = new CellBuffer(s_RedirectionHandler.GetBufferWidth(),
                    s_RedirectionHandler.GetBufferHeight(), s_CellBuffer);
            }
        }
		
		private static void Reset()
		{
			CursorTop = 0;
            CursorLeft = 0;
            s_MaxRow = 0;
            ResetColor();
            Clear();
		}

        private static bool HandleControlCharacter(char value)
        {
            bool preventBufferWrite = false;

            switch (value)
            {
                case '\n':
                    {
                        preventBufferWrite = true;

                        if (InternalCursorTop + 1 != s_CellBuffer.BufferHeight)
                            InternalCursorTop++;

                        BufferWrite('\r', true);

                        break;
                    }

                case '\t':
                    {
                        int length = (8 - (CursorLeft % 8));

                        for (int i = 0; i < length; i++)
                            Write(' ');

                        preventBufferWrite = true;
                        break;
                    }

                case '\b':
                    {
                        preventBufferWrite = true;

                        if (InternalCursorLeft - 1 >= 0)
                            InternalCursorLeft--;
                        
                        break;
                    }

                case '\r':
                    {
                        InternalCursorLeft = 0;
                        preventBufferWrite = true;
                        break;
                    }

                default: return false;
            }

            CheckFullBuffer();

            UpdateCursorPosition();

            return preventBufferWrite;
        }

        private static void CheckFullBuffer()
        {
            if (InternalCursorLeft >= s_CellBuffer.BufferWidth)
            {
                InternalCursorLeft = 0;
                InternalCursorTop++;
            }

            if (InternalCursorTop >= s_CellBuffer.BufferHeight)
            {
                InternalCursorTop = s_CellBuffer.BufferHeight - 1;

                ScrollBuffer(1, s_RefreshOnScroll);

#if MONO
				// Xterm does not scroll automatically, writing a newline causes it to scroll
                if (!IsWindows)
					AppendChar(CreateDefaultCell('\n'));
#endif

                UpdateCursorPosition();

                return;
            }
        }

        private static void BufferWrite(char value, bool writeToOut)
        {
            CheckBufferSize();

            CharacterCell cell = CreateDefaultCell(value);

            if (!HandleControlCharacter(cell.Character))
            {
                s_CellBuffer[InternalCursorLeft, InternalCursorTop] = cell;
                InternalCursorLeft++;

                if (writeToOut)
                {
                    CheckInitialization();
                    AppendChar(cell);
                }
            }

            CheckFullBuffer();
        }

        private static void UpdateMaxRow()
        {
            s_MaxRow = Math.Min(Math.Max(s_CursorRow, s_MaxRow), s_CellBuffer.BufferHeight - 1);
        }

        private static int InternalCursorLeft
        {
            get { return s_CursorCol; }
            set { s_CursorCol = value; }
        }

        private static int InternalCursorTop
        {
            get { return s_CursorRow; }
            set { s_CursorRow = value; UpdateMaxRow(); }
        }

        private static void Init(int width, int height)
        {
            if (width <= 0 || height <= 0)
                throw new ArgumentOutOfRangeException(String.Format("The specified buffer size is too small for use! ({0}, {1})",
                    new object[] { width, height }));

            s_CellBuffer = new CellBuffer(width, height);

            s_ErrorWriter = new NTermTextWriter();
            s_InReader = s_RedirectionHandler.In;
            s_OutWriter = new NTermTextWriter();

            Reset();
        }
		
		private static CharacterCell CreateDefaultCell(char val)
		{
			CharacterCell cell = new CharacterCell();
			cell.BackgroundColor = s_CurrentBackgroundColor;
			cell.BoregroundColor = s_CurrentForgroundColor;
			cell.CellType = CellType.Single;
			cell.Character = val;
			return cell;
		}
		
		private static void AppendChar(CharacterCell cell)
		{
			s_RedirectionHandler.AppendOutput(new CharacterCell[] { cell });
		}

        #endregion

        #region /// Internal ...

        internal static HostHandler CurrentRedirectionHandler
        {
            get { return s_RedirectionHandler; }
            set { s_RedirectionHandler = value; }
        }

        internal static bool IsWindows
        {
            get
            {
                if (System.Environment.OSVersion.Platform == PlatformID.Win32NT ||
                    System.Environment.OSVersion.Platform == PlatformID.Win32S ||
                    System.Environment.OSVersion.Platform == PlatformID.Win32Windows ||
                    System.Environment.OSVersion.Platform == PlatformID.WinCE ||
                    System.Environment.OSVersion.Platform == PlatformID.Xbox)
                {
                    return true;
                }
                else
                    return false;
            }
        }

        #endregion
    }
}
